﻿namespace SenenFernandez.Sensktop
{
    using SenenFernandez.Sensktop.Configuration;
    using SenenFernandez.Sensktop.Enum;

    using SenenFernandez.SmartExtensionMethods;
    using SenenFernandez.SmartExtensionMethods.Enum;
    using SenenFernandez.SmartExtensionMethods.Interactivity;

    using Microsoft.Win32;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.IO;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Threading;
    using System.Windows.Interop;
    using System.Xml;
    using System.Xml.Serialization;
    using System.Timers;

    internal class SensktopManager : NotificationObject
    {
        private readonly Window shell;
        private readonly Rect shellRect;
        private HookHandler callback;
        private IntPtr hookId;
        private bool ctrlpressed;
        private bool altpressed;
        private delegate IntPtr HookHandler(int code, int controlKey, ref HookData data);
        private delegate void KeyboardHookEventHandler(KeyboardHookEventArgs e);
        private delegate void ResizeDelegate(Window window, Rect rect);
        private bool isEnableWinButton;
        private bool isEnableAltTab;
        private bool isEnableAltEsc;
        private bool isEnableAltSpace;
        private bool isEnableAltF4;
        private ICommand closeCommand;
        private ICommand openTaskManagerCommand;
        private ICommand closeTaskManagerCommand;
        private ICommand openWindowsTaskManagerCommand;
        private Timer refresher;
        private Application windowsTaskManager;

        public string Title
        {
            get { return Properties.Resources.ApplicationTitle; }
        }

        public bool IsEnableWinButton
        {
            get { return isEnableWinButton; }
            set { SetValue(() => IsEnableWinButton, ref isEnableWinButton, value); }
        }

        public bool IsEnableAltTab
        {
            get { return isEnableAltTab; }
            set { SetValue(() => IsEnableAltTab, ref isEnableAltTab, value); }
        }

        public bool IsEnableAltEsc
        {
            get { return isEnableAltEsc; }
            set { SetValue(() => IsEnableAltEsc, ref isEnableAltEsc, value); }
        }

        public bool IsEnableAltSpace
        {
            get { return isEnableAltSpace; }
            set { SetValue(() => IsEnableAltSpace, ref isEnableAltSpace, value); }
        }

        public bool IsEnableAltF4
        {
            get { return isEnableAltF4; }
            set { SetValue(() => IsEnableAltF4, ref isEnableAltF4, value); }
        }

        public DateTime CurrentTime { get; private set; }

        public IEnumerable<Application> Applications { get; private set; }

        public IEnumerable<Application> ActiveApplications
        {
            get
            {
                return Applications.SelectHierarchy(list => list.Applications,
                    source => source.ApplicationStatus != ApplicationStatus.None);
            }
        }

        public ICommand CloseCommand
        {
            get { return closeCommand ?? (closeCommand = new DelegateCommand<Window>(OnClose)); }
        }

        public ICommand OpenTaskManagerCommand
        {
            get { return openTaskManagerCommand ?? (openTaskManagerCommand = new DelegateCommand(OpenTaskManager)); }
        }

        public ICommand CloseTaskManagerCommand
        {
            get { return closeTaskManagerCommand ?? (closeTaskManagerCommand = new DelegateCommand(CloseTaskManager)); }
        }

        public ICommand OpenWindowsTaskManagerCommand
        {
            get
            {
                return openWindowsTaskManagerCommand ??
                       (openWindowsTaskManagerCommand = new DelegateCommand<string>(OpenWindowsTaskManager));
            }
        }

        /// <summary>
        /// Sets up a keyboard hook to trap all keystrokes without passing any to other applications.
        /// </summary>
        public SensktopManager()
        {
            CurrentTime = DateTime.Now;
            shell = new MainView
            {
                DataContext = this
            };
            shell.Loaded += OnShellLoaded;
            shell.Closed += OnShellClosed;
            shellRect = new Rect(shell.Left, shell.Top, shell.Width, shell.Height);
            shell.Show();
            SetPosition(Position.Bottom);
            if (SensktopConfiguration.Instance.TakeFullControl)
            {
                Wpf.InvokeAsync(() =>
                {
                    ShowWindows(ShowStyle.Hide);
                    hookId = IntPtr.Zero;
                    callback = HookCallback;
                    using (var process = Process.GetCurrentProcess())
                    {
                        using (var mainModule = process.MainModule)
                        {
                            hookId = SetWindowsHookEx(13, callback, GetModuleHandle(mainModule.ModuleName), 0);
                        }
                    }
                });
            }
            SystemEvents.SessionSwitch += OnSessionSwitch;
            SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public override void Dispose()
        {
            refresher.Enabled = false;
            SystemEvents.DisplaySettingsChanged -= OnDisplaySettingsChanged;
            SystemEvents.SessionSwitch -= OnSessionSwitch;
            SetPosition(Position.None);
            if (SensktopConfiguration.Instance.TakeFullControl)
            {
                UnhookWindowsHookEx(hookId);
                ShowWindows(ShowStyle.Show);
            }
            ProcessExplorer.Instance.Shutdown();
        }

        private void OnClose(Window window)
        {
            if (window != null)
            {
                window.Close();
            }
        }

        private Window TaskManagerWindow;

        private void OpenTaskManager()
        {
            if (TaskManagerWindow == null)
            {
                TaskManagerWindow = new Window
                {
                    Title = Properties.Resources.TaskManagerTitle,
                    WindowStartupLocation = WindowStartupLocation.CenterScreen,
                    DataContext = this,
                    Owner = shell,
                    Style = DefaultValue.PopupWindowStyle.FindResource<Style>(),
                    Template = DefaultValue.TaskManagerViewTemplate.FindResource<ControlTemplate>(),
                };
                EventHandler[] handler = { null };
                handler[0] = (sender, e) =>
                {
                    TaskManagerWindow.Closed -= OnTaskManagerWindowClosed;
                };
                TaskManagerWindow.Closed += OnTaskManagerWindowClosed;
            }
            TaskManagerWindow.RestoreWindow();
        }

        private void OnTaskManagerWindowClosed(object sender, EventArgs e)
        {
            TaskManagerWindow = null;
        }

        private void CloseTaskManager()
        {
            if (TaskManagerWindow != null)
            {
                TaskManagerWindow.Close();
            }
        }

        private void OpenWindowsTaskManager(string parameter)
        {
            if (windowsTaskManager == null)
            {
                windowsTaskManager = new Application
                {
                    FileName = parameter,
                    ApplicationType = ApplicationType.Console,
                };
            }
            windowsTaskManager.Open();
        }

        private void OnShellLoaded(object sender, RoutedEventArgs e)
        {
            refresher = new Timer(TimeSpan.FromMilliseconds(SensktopConfiguration.Instance.RefreshInterval).TotalMilliseconds);
            refresher.Elapsed += OnRefresherElapsed;
            refresher.Enabled = true;
            LoadMenu();
        }

        private void OnShellClosed(object sender, EventArgs e)
        {
            Dispose();
        }

        private void OnRefresherElapsed(object sender, ElapsedEventArgs e)
        {
            SetValue(() => CurrentTime, e.SignalTime);
        }

        private void OnDisplaySettingsChanged(object sender, EventArgs e)
        {
            WorkingArea.Instance.SetDisplay(true);
            if (shell != null)
            {
                shell.Left = WorkingArea.Instance.Left;
                shell.Top = WorkingArea.Instance.DesktopTop;
                shell.Width = WorkingArea.Instance.Width;
                shell.Height = WorkingArea.Instance.DesktopHeight;
                if (SensktopConfiguration.Instance.TakeFullControl)
                {
                    ShowWindows(ShowStyle.Hide);
                }
                ProcessExplorer.Instance.ResizeApplications();
            }
        }

        private void OnSessionSwitch(object sender, SessionSwitchEventArgs e)
        {
            if (shell != null)
            {
                RestoreWindow();
            }
        }

        private XmlReader CreateMenuXmlReader()
        {
            return File.Exists(SensktopConfiguration.Instance.MenuSourcePath)
                ? XmlReader.Create(SensktopConfiguration.Instance.MenuSourcePath)
                : XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), DefaultValue.MenuSourcePath));
        }

        private void LoadMenu()
        {
            Wpf.InvokeAsync(() =>
            {
                try
                {
                    var serializer = new XmlSerializer(typeof (Menu));
                    Menu menu = null;
                    using (var reader = CreateMenuXmlReader())
                    {
                        if (reader != null)
                        {
                            menu = serializer.Deserialize(reader) as Menu;
                        }
                    }
                    if (menu != null)
                    {
                        var applications = new List<Application>();
                        foreach (var application in menu.Applications)
                        {
                            AddOnPropertyChanged(application);
                            SetParent(application);
                            applications.Add(application);
                            if (application.IsAutorun)
                            {
                                var applicationAsync = application;
                                Wpf.InvokeAsync(applicationAsync.Autorun);
                            }
                        }
                        SetApplications(applications);
                    }
                }
                catch (Exception exception)
                {
                    exception.Log();
                }
            });
        }

        private void SetApplications(IEnumerable<Application> value)
        {
            if (value != Applications)
            {
                Applications = value;
                RaisePropertyChanged(() => Applications);
            }
        }

        private void SetParent(Application parent)
        {
            foreach (var application in parent.Applications)
            {
                application.SetParent(parent);
                SetParent(application);
            }
        }

        private void AddOnPropertyChanged(Application application)
        {
            application.PropertyChanged += OnApplicationPropertyChanged;
            if (application.HasChildApplications)
            {
                foreach (var child in application.Applications)
                {
                    AddOnPropertyChanged(child);
                }
            }
        }

        private void OnApplicationPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName.Equals(DefaultValue.ApplicationStatus))
            {
                RaisePropertyChanged(() => ActiveApplications);
            }
        }

        private void SetPosition(Position position)
        {
            if (shell != null)
            {
                var data = new Data();
                data.size = Marshal.SizeOf(data);
                data.handle = new WindowInteropHelper(shell).Handle;
                if (position == Position.None)
                {
                    SHAppBarMessage(AbMsg.Remove, ref data);
                }
                else
                {
                    data.callbackMessage = DefaultValue.AppBarMessage.RegisterWindowMessage();
                    SHAppBarMessage(AbMsg.New, ref data);
                    data.position = position;
                    data.area.left = (int) Math.Round(WorkingArea.Instance.Left);
                    data.area.right = (int) Math.Round(WorkingArea.Instance.Width);
                    data.area.bottom = (int) Math.Round(WorkingArea.Instance.Height);
                    data.area.top = data.area.bottom - (int) Math.Round(shell.ActualHeight);
                    SHAppBarMessage(AbMsg.SetPos, ref data);
                    RestoreWindow();
                }
            }
        }

        private void ShowWindows(ShowStyle showStyle)
        {
            DefaultValue.ButtonWindow.FindWindow().ShowWindow(showStyle);
            DefaultValue.ImmersiveLauncherWindow.FindWindow().ShowWindow(showStyle);
            DefaultValue.ShellTrayWindow.FindWindow().ShowWindow(showStyle);
            DefaultValue.ShellSecondaryTrayWindow.FindWindow().ShowWindow(showStyle);
            DefaultValue.ProgmanWindow.FindWindow().EnableWindow(showStyle != ShowStyle.Hide);
        }

        private void RestoreWindow()
        {
            if (shell != null)
            {
                shell.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new ResizeDelegate(DoResize), shell, shellRect);
            }
        }

        private void DoResize(Window window, Rect rect)
        {
            window.Top = rect.Top;
            window.Left = rect.Left;
            window.Width = rect.Width;
            window.Height = rect.Height;
        }

        private IntPtr HookCallback(int code, int controlKey, ref HookData data)
        {
            bool hooked;
            switch (controlKey)
            {
                case ControlKey.KeyDown:
                case ControlKey.KeyUp:
                case ControlKey.SysKeyDown:
                case ControlKey.SysKeyUp:
                    {
                        if (controlKey == ControlKey.KeyDown)
                        {
                            if (data.KeyCode == ControlKey.CtrlFlag)
                            {
                                ctrlpressed = true;
                            }

                            if (data.KeyCode == ControlKey.AltFlag)
                            {
                                altpressed = true;
                            }
                        }
                        if (controlKey == ControlKey.KeyUp)
                        {
                            if (data.KeyCode == ControlKey.CtrlFlag)
                            {
                                ctrlpressed = false;
                            }
                            if (data.KeyCode == ControlKey.AltFlag)
                            {
                                altpressed = false;
                            }
                        }
                        hooked = ((data.Flags == ControlKey.Win && data.KeyCode == ControlKey.LWin && !IsEnableWinButton)
                            | (data.Flags == ControlKey.Win && data.KeyCode == ControlKey.RWin && !IsEnableWinButton)
                            | (data.Flags == ControlKey.Alt && data.KeyCode == ControlKey.Esc && !IsEnableAltEsc)
                            | (data.Flags == ControlKey.Alt && data.KeyCode == ControlKey.Space && !IsEnableAltSpace)
                            | (data.Flags == ControlKey.Alt && data.KeyCode == ControlKey.F4 && !IsEnableAltF4)
                            | (data.Flags == ControlKey.Alt && data.KeyCode == ControlKey.Tab && !IsEnableAltTab)
                            );

                        if (ctrlpressed && !hooked && !IsEnableWinButton)
                        {
                            hooked = (data.KeyCode == ControlKey.Esc) && (data.Flags == 0);
                        }
                        if (ctrlpressed && altpressed && data.KeyCode == ControlKey.Supr && data.Flags == 1)
                        {
                            ctrlpressed = false;
                            altpressed = false;
                        }
                        break;
                    }
                default:
                    {
                        hooked = false;
                        break;
                    }
            }
            return hooked ? new IntPtr(1) : CallNextHookEx(hookId, code, controlKey, ref data);
        }

        [DllImport(DefaultValue.Kernel32, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string moduleName);

        [DllImport(User32.Path, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookHandler hookHandler, IntPtr handle, uint threadId);

        [DllImport(User32.Path, CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr handle);

        [DllImport(User32.Path, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr handle, int code, int controlKey, ref HookData data);

        [DllImport(DefaultValue.Shell32, CallingConvention = CallingConvention.StdCall)]
        private static extern uint SHAppBarMessage(AbMsg message, ref Data data);

        private class KeyboardHookEventArgs : EventArgs
        {
            /// <summary>
            /// The name of the key that was pressed.
            /// </summary>
            public Key Key { get; private set; }

            /// <summary>
            /// The virtual key code of the key that was pressed.
            /// </summary>
            public int KeyCode { get; private set; }

            /// <summary>
            /// True if this key combination was passed to other applications,
            /// false if it was trapped.
            /// </summary>
            public bool PassThrough { get; private set; }

            /// <summary>
            /// Initializes a new instance of the KeyboardHookEventArgs class.
            /// </summary>
            public KeyboardHookEventArgs(int keyCode, bool passThrough)
            {
                Key = KeyInterop.KeyFromVirtualKey(keyCode);
                KeyCode = keyCode;
                PassThrough = passThrough;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct Area
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct Data
        {
            public int size;
            public IntPtr handle;
            public int callbackMessage;
            public Position position;
            public Area area;
        }

        private struct ControlKey
        {
            public const int KeyDown = 256;
            public const int KeyUp = 257;
            public const int SysKeyDown = 260;
            public const int SysKeyUp = 261;
            public const int CtrlFlag = 162;
            public const int AltFlag = 164;
            public const int Alt = 32;
            public const int Tab = 9;
            public const int Esc = 27;
            public const int Win = 1;
            public const int LWin = 91;
            public const int RWin = 92;
            public const int F4 = 115;
            public const int Space = 32;
            public const int Supr = 46;
        }

        private struct HookData
        {
            public int KeyCode;
            public int ScanCode;
            public int Flags;
            public int Time;
            public int ExtraInfo;
        }
    }
}